package net.avcompris.binding.yaml.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static com.avcompris.util.YamlUtils.getKey;
import static com.avcompris.util.YamlUtils.getObjectProperty;
import static com.avcompris.util.YamlUtils.getPropertyKeys;
import static org.apache.commons.lang3.StringUtils.isBlank;

import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.jaxen.yaml.YamlXPath;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.avcompris.lang.NotImplementedException;

/**
 * YAML-DOM converter.
 *
 * @author David Andrianavalontsalama
 */
public class DomYamlConverter {

	/**
	 * transform a YAML object into a DOM node.
	 * @throws ParserConfigurationException 
	 */
	public static Node yamlToNode(final Object yaml,
			final BindConfiguration configuration) throws ParserConfigurationException {

		nonNullArgument(yaml, "yaml");
		nonNullArgument(configuration, "configuration");

		final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
				.newInstance();

		//documentBuilderFactory.setNamespaceAware(true);

		final DocumentBuilder documentBuilder = documentBuilderFactory
				.newDocumentBuilder();

		final Document document = documentBuilder.newDocument();

		final DomYamlConverter converter = new DomYamlConverter(document,
				configuration);

		return converter.parseYaml(yaml);
	}

	private final Document document;
	private final BindConfiguration configuration;

	private DomYamlConverter(
			final Document document,
			final BindConfiguration configuration) {

		this.document = nonNullArgument(document);
		this.configuration = nonNullArgument(configuration);
	}

	private Node parseYaml(final Object yaml) {

		nonNullArgument(yaml, "yaml");

		final String rootElementName = getKey(yaml);

		final Element rootElement = createElement(rootElementName);

		document.appendChild(rootElement);

		final Object value = getObjectProperty(yaml, rootElementName);

		if (value instanceof Map<?, ?>) {

			parseYaml(rootElement, (Map<?, ?>) value);

		} else if (value instanceof List<?>) {

			parseYaml(rootElement, (List<?>) value);
		}

		return document.getDocumentElement();
	}

	private void parseYaml(final Element element, Map<?, ?> yaml) {

		nonNullArgument(element, "element");
		nonNullArgument(yaml, "yaml");

		for (final Object key : getPropertyKeys(yaml)) {

			final boolean isName = String.class.isInstance(key);

			final Object value = getObjectProperty(yaml, key);

			if (value == null) {

				// do nothing

				//				throw new NotImplementedException(propertyName
				//						+ ".value == null");

			} else if (value instanceof String || value instanceof Integer
					|| value instanceof Long || value instanceof Boolean) {

				final String s = value.toString();

				if (configuration.isNodesElementsEverywhere()) {

					final Element child;

					if (configuration.isNodesElementNames() && isName) {

						final String name = (String) key;

						child = createElement(name);

					} else {

						child = createElementNS("key");

						if (isName) {

							final String name = (String) key;

							addAttribute(child, "name", name);

						} else if (List.class.isInstance(key)) {

							element.appendChild(child);

							parseYaml(child, (List<?>) key);

							final Element valueElement = createElementNS("value");

							child.appendChild(valueElement);

							appendTextNode(valueElement, s);

							continue;

						} else {

							throw new NotImplementedException("key.type: "
									+ key.getClass().getName());
						}
					}

					element.appendChild(child);

					appendTextNode(child, s);

				} else if (!configuration.isNodesEmptyAttributes()
						&& isBlank(s)) {

					// do nothing

				} else {

					if (!isName) {
						throw new NotImplementedException("key != String");
					}

					final String name = (String) key;

					addAttribute(element, name, s);
				}

			} else if (value instanceof Map<?, ?>) {

				//	if (!isName) {
				//	throw new NotImplementedException("key != String");
				//}

				final Element child;

				if (configuration.isNodesElementNames() && isName) {

					final String name = (String) key;

					child = createElement(name);

				} else {

					child = createElementNS("key");

					if (isName) {

						final String name = (String) key;

						addAttribute(child, "name", name);

					} else if (List.class.isInstance(key)) {

						element.appendChild(child);

						parseYaml(child, (List<?>) key);

						final Element valueElement = createElementNS("value");

						child.appendChild(valueElement);

						parseYaml(valueElement, (Map<?, ?>) value);

						continue;

					} else {

						throw new NotImplementedException("key.type: "
								+ key.getClass().getName());
					}
				}

				element.appendChild(child);

				parseYaml(child, (Map<?, ?>) value);

			} else if (value instanceof List<?>) {

				final Element parent;

				if (configuration.isNodesElementNames()) {

					parent = element;

				} else {

					parent = createElementNS("key");

					if (isName) {

						final String name = (String) key;

						addAttribute(parent, "name", name);
					}

					element.appendChild(parent);
				}

				for (final Object o : (List<?>) value) {

					if (!isName) {
						throw new NotImplementedException("key != String");
					}

					final String name = (String) key;

					final Element child;

					if (configuration.isNodesElementNames()) {

						child = createElement(name);

					} else {

						child = createElementNS("item");

						if (o instanceof String) {

							appendTextNode(child, (String) o);

						} else if (o instanceof Map<?, ?>) {

							parseYaml(child, (Map<?, ?>) o);
						}

						//addAttribute(child, "name", name);
					}

					parent.appendChild(child);

					if (o instanceof Map<?, ?>) {

						parseYaml(child, (Map<?, ?>) o);
					}
				}

			} else {

				// System.out.println(value.getClass());
				// do nothing
			}
		}
	}

	private void parseYaml(final Element element, List<?> yaml) {

		nonNullArgument(element, "element");
		nonNullArgument(yaml, "yaml");

		for (final Object o : yaml) {

			final Element child = createElementNS(
			//	YamlXPath.NamespaceURI.XMLNSURI_AVC_BINDING_YAML.getURI(),
			"item");

			element.appendChild(child);

			if (o instanceof String) {

				appendTextNode(child, (String) o);

			} else if (o instanceof Map<?, ?>) {

				parseYaml(child, (Map<?, ?>) o);
			}
		}
	}

	private void addAttribute(final Element element, final String name,
			final String value) {

		nonNullArgument(element, "element");
		nonNullArgument(name, "name");
		nonNullArgument(value, "value");

		try {

			element.setAttribute(name, value);

		} catch (final DOMException e) {

			throw new DomYamlConversionException("Cannot addAttribute(\""
					+ name + "\", \"" + value + "\")", e);
		}
	}

	private Element createElement(final String name) {

		nonNullArgument(name, "name");

		try {

			return document.createElement(name);

		} catch (final DOMException e) {

			throw new DomYamlConversionException("Cannot createElement(\""
					+ name + "\")", e);
		}
	}

	private Element createElementNS(//final String uri, 
			final String name) {

		//nonNullArgument(uri, "uri");
		nonNullArgument(name, "name");

		final String uri = YamlXPath.NamespaceURI.XMLNSURI_AVC_BINDING_YAML
				.getURI();

		try {

			return document.createElementNS(uri, name);

		} catch (final DOMException e) {

			throw new DomYamlConversionException("Cannot createElementNS(\"{"
					+ uri + "}\", \"" + name + "\")", e);
		}
	}

	private void appendTextNode(final Element element, final String value) {

		nonNullArgument(element, "element");
		nonNullArgument(value, "value");

		element.appendChild(document.createTextNode(value));
	}
}
